/*
 * @Descripttion: 坐标转化工具
 * @version: 1.0.0
 * @Date: 2020-11-17 09:55:50
 * @LastEditTime: 2022-06-21 10:21:21
 */
import type {
  Cartesian2,
  Ellipsoid,
  Viewer
} from "cesium"
import type { IPoint2D } from "../CustomCesium"
import {
  Cartesian3,
  Cartographic,
  Math as CesiumMath,
  defined,
  Matrix3,
  Matrix4,
  Transforms
} from "cesium"
import { GCS } from "../CustomCesium"
import Constant from "./Constant"

const xPI = (Math.PI * 3000.0) / 180.0
const a = 6378245.0
const ee = 0.006693421622965943

/**
 * @descripttion: 转化工具
 */
class MathUtils {
  /**
   * @descripttion: 笛卡尔坐标转换为经纬度
   * @param {Viewer} viewer 视图
   * @param {Cartesian3} cartesian3 笛卡尔坐标
   * @return {*}
   */
  static car3ToGCS(viewer: Viewer, cartesian3: Cartesian3) {
    const ellipsoid = viewer.scene.globe.ellipsoid
    const cartographic = ellipsoid.cartesianToCartographic(cartesian3)
    const lat = CesiumMath.toDegrees(cartographic.latitude)
    const lon = CesiumMath.toDegrees(cartographic.longitude)
    const alt = cartographic.height
    const gcs = new GCS(lon, lat, alt)
    return gcs
  }

  /**
   * @descripttion: 世界坐标转换为中文经纬度
   * @param {Viewer} viewer 视图
   * @param {Cartesian3} cartesian3 世界坐标
   * @param {number} figure 小数位数
   * @param {boolean} isString 是否字符串
   * @return {string}
   */
  static car3ToGCSInCHN(
    viewer: Viewer,
    cartesian3: Cartesian3,
    figure: number,
    isString: boolean
  ) {
    if (!defined(cartesian3)) return undefined
    const ellipsoid = viewer.scene.globe.ellipsoid
    const cartographic = ellipsoid.cartesianToCartographic(cartesian3)
    const lat = figure
      ? Number(CesiumMath.toDegrees(cartographic.latitude)).toFixed(figure)
      : CesiumMath.toDegrees(cartographic.latitude)
    const lon = figure
      ? Number(CesiumMath.toDegrees(cartographic.longitude)).toFixed(figure)
      : CesiumMath.toDegrees(cartographic.longitude)
    const alt = figure
      ? Number(cartographic.height).toFixed(figure)
      : cartographic.height
    return isString
      ? `经度：${lon}   纬度：${lat}   高度：${alt}`
      : { 经度: lon, 纬度: lat, 高度: alt }
  }

  /**
   * @descripttion: 经纬度转为世界坐标
   * @param {GCS} gcs 地理坐标
   * @param {Ellipsoid} ellipsoid 椭球体
   * @return {*}
   */
  static gcsToCartesian3(gcs: GCS, ellipsoid?: Ellipsoid) {
    return Cartesian3.fromDegrees(
      gcs.longitude,
      gcs.latitude,
      gcs.height,
      ellipsoid
    )
  }

  /**
   * gcs转弧度坐标
   * @param gcs
   * @returns
   */
  static gcsToCartographic(gcs: GCS) {
    return gcs
      ? Cartographic.fromDegrees(
          gcs.longitude,
          gcs.latitude,
          gcs.height
        )
      : Cartographic.ZERO
  }

  /**
   * @descripttion: 笛卡尔坐标转换为经纬度
   * @param {Viewer} viewer 视图
   * @param {Cartesian3} cartesian3 笛卡尔坐标
   * @return {*}
   * ssr
   */
  static car3ToLonLat(viewer: Viewer, cartesian3: Cartesian3[]): IPoint2D {
    const ellipsoid = viewer.scene.globe.ellipsoid
    const cartographic = ellipsoid.cartesianToCartographic(cartesian3)
    const lat = CesiumMath.toDegrees(cartographic.latitude)
    const lon = CesiumMath.toDegrees(cartographic.longitude)
    return [lon, lat]
  }

  /**
   * @descripttion: 笛卡尔坐标数组转换为经纬度数组
   * @param {Viewer} viewer 视图
   * @param {Cartesian3} cartesian3 笛卡尔坐标
   * @return {*}
   * ssr
   */
  static car3ToLonLatArray(viewer: Viewer, cartesian3: Cartesian3[]): IPoint2D[] {
    const ellipsoid = viewer.scene.globe.ellipsoid
    let result: IPoint2D[] = []
    cartesian3.forEach((item) => {
      let cartographic = ellipsoid.cartesianToCartographic(item)
      let lat = CesiumMath.toDegrees(cartographic.latitude)
      let lon = CesiumMath.toDegrees(cartographic.longitude)
      result.push([lon, lat])
    })
    return result
  }

  /**
   * @descripttion: 计算两点间向量（三维）
   * @param {Cartesian3} left 左边点
   * @param {Cartesian3} right 右边点
   * @return {Cartesian3} 单位向量
   */
  static getVector(left: Cartesian3, right: Cartesian3) {
    if (!defined(left) || !defined(right)) return undefined
    const distance = new Cartesian3()
    const vector = new Cartesian3()
    Cartesian3.subtract(left, right, distance)
    Cartesian3.normalize(distance, vector)
    return vector
  }

  /**
   * @descripttion: 两点间距离（二维）
   * @param {IPoint} target
   * @param {IPoint} origin
   * @return {*}
   */
  static distance(target: IPoint2D, origin: IPoint2D) {
    return Math.sqrt(
      (target[0] - origin[0]) ** 2 + (target[1] - origin[1]) ** 2
    )
  }

  /**
   * @descripttion: 计算中点
   * @param {IPoint2D} t
   * @param {IPoint2D} o
   * @return {*}
   */
  static mid(t: IPoint2D, o: IPoint2D) {
    return [(t[0] + o[0]) / 2, (t[1] + o[1]) / 2] as IPoint2D
  }

  /**
   * @descripttion: 各点间的距离累计和（二维）
   * @param {Array<IPoint>} path
   * @return {*}
   */
  static wholeDistance(path: Array<IPoint2D>) {
    let distance = 0
    for (let index = 0; index < path.length - 1; index++)
      distance += this.distance(path[index], path[index + 1])
    return distance
  }

  /**
   * @descripttion: 路径长度（二维）
   * @param {Array<IPoint>} path
   * @return {*}
   */
  static getBaseLength(path: Array<IPoint2D>) {
    return this.wholeDistance(path) ** 0.99
  }

  /**
   * @descripttion: 计算方位角（弧度制）
   * @param {IPoint} target 目标点位
   * @param {IPoint} origin 源点位
   * @return {*}
   */
  static getAzimuth(target: IPoint2D, origin: IPoint2D) {
    let e = 0
    const r = Math.asin(
      Math.abs(origin[1] - target[1]) / this.distance(target, origin)
    )

    origin[1] >= target[1] && origin[0] >= target[0]
      ? (e = r + Math.PI)
      : origin[1] >= target[1] && origin[0] < target[0]
        ? (e = Constant.TWO_PI - r)
        : origin[1] < target[1] && origin[0] < target[0]
          ? (e = r)
          : origin[1] < target[1] && origin[0] >= target[0] && (e = Math.PI - r)

    return e
  }

  /**
   * @descripttion: 根据参数计算第三点位置
   * @param {IPoint} target 目标点
   * @param {IPoint} origin 源点
   * @param {number} angle 旋转角度
   * @param {number} factor 旋转因子
   * @param {boolean} clockwise 顺时针
   * @return {*}
   */
  static getThirdPoint(
    target: IPoint2D,
    origin: IPoint2D,
    angle: number,
    factor: number,
    clockwise: boolean
  ) {
    const g = this.getAzimuth(target, origin)
    const i = clockwise ? g + angle : g - angle
    const s = factor * Math.cos(i)
    const a = factor * Math.sin(i)
    return [origin[0] + s, origin[1] + a] as IPoint2D
  }

  /**
   * @descripttion: 阶乘
   * @param {*} t
   * @return {*}
   */
  static getFactorial(origin: number) {
    if (origin <= 1) return BigInt(1)
    if (origin == 2) return BigInt(2)
    if (origin == 3) return BigInt(6)
    if (origin == 4) return BigInt(24)
    if (origin == 5) return BigInt(120)
    let res = BigInt(1)
    for (let e = BigInt(1); origin >= e; e++) res *= e
    return res
  }

  /**
   * @descripttion: 二项式因子
   * @param {number} x
   * @param {number} y
   * @return {*}
   */
  static getBinomialFactor(x: number, y: number) {
    const res
      = this.getFactorial(x) / (this.getFactorial(y) * this.getFactorial(x - y))
    return res
  }

  /**
   * @descripttion: 三次贝塞尔插值
   * @param {Array} points 轨迹关键点
   * @param {*} numberOfPoints 插值点数
   * @return {*}
   */
  static getThreeBezier(
    points: Array<IPoint2D>,
    numberOfPoints = 40
  ): Array<IPoint2D> {
    const bezierLinePoints: IPoint2D[] = []
    // 步长
    const dt = 1.0 / (numberOfPoints - 1)
    const length = points.length
    const inteval = Math.floor(length / 4) // 向下取整 找下标
    /* 计算多项式系数  三次贝塞尔的计算公式 */
    const cx = 3.0 * (points[inteval][0] - points[0][0])
    const bx = 3.0 * (points[2 * inteval][0] - points[inteval][0]) - cx
    const ax = points[length - 1][0] - points[0][0] - cx - bx
    const cy = 3.0 * (points[inteval][1] - points[0][1])
    const by = 3.0 * (points[2 * inteval][1] - points[inteval][1]) - cy
    const ay = points[length - 1][1] - points[0][1] - cy - by

    for (let i = 0; i < numberOfPoints; i++) {
      // 点位度量
      const scale = i * dt
      const tSquared = scale ** 2 // t^2
      const tCubed = scale ** 3 // t^3

      // 曲线上的每个记忆点都对应一个scale
      // scale的范围在[0-1]
      bezierLinePoints.push([
        ax * tCubed + bx * tSquared + cx * scale + points[0][0],
        ay * tCubed + by * tSquared + cy * scale + points[0][1]
      ])
    }
    return bezierLinePoints
  }

  /**
   * @descripttion: 根据三个点计算夹角
   * @param {*} t
   * @param {*} o
   * @param {*} e
   * @return {*}
   */
  static getAngleOfThreePoints(t: IPoint2D, o: IPoint2D, e: IPoint2D) {
    const r = this.getAzimuth(o, t) - this.getAzimuth(o, e)
    return r < 0 ? r + Constant.TWO_PI : r
  }

  /**
   * @descripttion: 判断是否为顺时针
   * @param {IPoint2D} target 目标点
   * @param {IPoint2D} origin 源点
   * @param {IPoint2D} reference 参照点
   * @return {*}
   */
  static isClockWise(target: IPoint2D, origin: IPoint2D, reference: IPoint2D) {
    return (
      (reference[1] - target[1]) * (origin[0] - target[0])
      > (origin[1] - target[1]) * (reference[0] - target[0])
    )
  }

  /**
   * @descripttion: 计算两点间偏航角
   * @param {GCS} posA A点坐标
   * @param {GCS} posB B点坐标
   * @return {*}
   */
  static getHeading(posA: GCS, posB: GCS) {
    // 地理坐标A,B
    const p0 = this.gcsToCartesian3(posA)
    const p1 = this.gcsToCartesian3(posB)
    if (!defined(p0) && defined(p1)) return undefined
    // 计算p0位置的enu位置矩阵的旋转部分
    const defrotmat = Matrix4.fromRotationTranslation(
      Transforms.eastNorthUpToFixedFrame(p0),
      Cartesian3.ZERO
    )
    // let defrotmat = Matrix4.fromTranslation(Transforms.eastNorthUpToFixedFrame(p0), new Matrix3());
    // let defrotmat = Transforms.eastNorthUpToFixedFrame(p0);

    // 获取矩阵的三个坐标轴
    const xaxis = Matrix3.getColumn(defrotmat, 0, new Cartesian3())
    const yaxis = Matrix3.getColumn(defrotmat, 1, new Cartesian3())
    const zaxis = Matrix3.getColumn(defrotmat, 2, new Cartesian3())

    // 两个位置点的射线方向
    let dir = Cartesian3.subtract(p1, p0, new Cartesian3())

    // 计算在 enu 旋转矩阵上的 x y 平面上的投影向量
    dir = Cartesian3.cross(dir, zaxis, dir)
    dir = Cartesian3.cross(zaxis, dir, dir)

    // 归一化
    dir = Cartesian3.normalize(dir, dir)

    // 计算和x轴夹角  0 ~  pi
    let heading = Cartesian3.angleBetween(xaxis, dir)

    // 和y轴夹角 判定在y轴的正方向还是负方向
    const ay = Cartesian3.angleBetween(yaxis, dir)

    // 保证处于0~2PI
    if (ay > Math.PI * 0.5) {
      heading = 2 * Math.PI - heading
    }

    return CesiumMath.toDegrees(heading)
  }

  /**
   * @descripttion: 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换（即 百度 转 谷歌、高德）
   * @param {GCS} gcs
   * @return {*}
   */
  static bd09ToGCJ02(gcs: GCS) {
    const xpi = (Math.PI * 3000.0) / 180.0
    const x = gcs.longitude - 0.0065
    const y = gcs.latitude - 0.006
    const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * xpi)
    const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * xpi)
    const gglng = z * Math.cos(theta)
    const gglat = z * Math.sin(theta)
    return new GCS(gglng, gglat)
  }

  /**
   * @descripttion: 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换（即谷歌、高德 转 百度）
   * @param lng
   * @param lat
   * @returns {*[]}
   */
  static gcj02ToBD09(gcs: GCS) {
    const z
      = Math.sqrt(gcs.longitude * gcs.longitude + gcs.latitude * gcs.latitude)
        + 0.00002 * Math.sin(gcs.latitude * xPI)
    const theta
      = Math.atan2(gcs.latitude, gcs.longitude)
        + 0.000003 * Math.cos(gcs.longitude * xPI)
    const bdlng = z * Math.cos(theta) + 0.0065
    const bdlat = z * Math.sin(theta) + 0.006
    return new GCS(bdlng, bdlat)
  }

  /**
   * @descripttion: WGS84转GCj02
   * @param {GCS} gcs 地理坐标
   * @return {*}
   */
  static wgs84ToGCJ02(gcs: GCS) {
    if (outofchina(gcs)) {
      return gcs
    }
    const retGCS = new GCS(gcs.longitude - 105.0, gcs.latitude - 35.0)
    const dGCS = transformGCS(retGCS)
    const radlat = (gcs.latitude / 180.0) * Math.PI
    let magic = Math.sin(radlat)
    magic = 1 - ee * magic * magic
    const sqrtmagic = Math.sqrt(magic)
    const dlat
        = (dGCS.latitude * 180.0)
          / (((a * (1 - ee)) / (magic * sqrtmagic)) * Math.PI)
    const dlng
        = (dGCS.longitude * 180.0)
          / ((a / sqrtmagic) * Math.cos(radlat) * Math.PI)
    const mglat = gcs.latitude + dlat
    const mglng = gcs.longitude + dlng
    // return [mglng - 0.00838994979859, mglat - 0.003615617752074]
    return new GCS(mglng, mglat)
  }

  /**
   * @descripttion: GCJ02 转换为 WGS84
   * @param {GCS} gcs
   * @return {*}
   */
  static gcj02ToWGS84(gcs: GCS) {
    if (outofchina(gcs)) {
      return gcs
    }
    const retGCS = new GCS(gcs.longitude - 105.0, gcs.latitude - 35.0)
    const dGCS = transformGCS(retGCS)
    const radlat = (gcs.latitude / 180.0) * Math.PI
    let magic = Math.sin(radlat)
    magic = 1 - ee * magic * magic
    const sqrtmagic = Math.sqrt(magic)
    const dlat
        = (dGCS.latitude * 180.0)
          / (((a * (1 - ee)) / (magic * sqrtmagic)) * Math.PI)
    const dlng
        = (dGCS.longitude * 180.0)
          / ((a / sqrtmagic) * Math.cos(radlat) * Math.PI)
    const mglat = gcs.latitude + dlat
    const mglng = gcs.longitude + dlng
    return new GCS(gcs.longitude * 2 - mglng, gcs.latitude * 2 - mglat)
  }

  /**
   * @descripttion: 度转度°分′秒″
   * @param {*} val
   * @return {*}
   */
  static toDMS(val: number) {
    if (typeof val == "undefined") {
      return null
    }
    const strVal = String(val)
    let i = strVal.indexOf(".")
    const strDu = i < 0 ? val : strVal.substring(0, i) // 获取度
    let strFen: number | string = 0
    let strMiao: number | string = 0

    if (i > 0) {
      strFen = Number(`0${strVal.substring(i)}`)
      strFen = strFen * 60
      i = (`${strFen}`).indexOf(".")
      if (i > 0) {
        strMiao = Number(`0${(`${strFen}`).substring(i)}`)
        strFen = (`${strFen}`).substring(0, i) // 获取分
        strMiao = `${strMiao * 60}`
        i = strMiao.indexOf(".")
        strMiao = strMiao.substring(0, i + 4) // 取到小数点后面三位
        strMiao = Number.parseFloat(strMiao).toFixed(2) // 精确小数点后面两位
      }
    }

    return {
      degrees: Number(strDu),
      minutes: Number(strFen),
      seconds: Number(strMiao)
    }
  }

  /**
   * @descripttion: 度°分′秒″转度
   * @param {*} strDu
   * @param {*} strFen
   * @param {*} strMiao
   * @param {*} len
   * @return {*}
   */
  static toDegree(strDu, strFen, strMiao, len) {
    len = len > 6 || typeof len == "undefined" ? 6 : len // 精确到小数点后最多六位
    strDu = typeof strDu == "undefined" || strDu == "" ? 0 : Number.parseFloat(strDu)
    strFen
      = typeof strFen == "undefined" || strFen == ""
        ? 0
        : Number.parseFloat(strFen) / 60
    strMiao
      = typeof strMiao == "undefined" || strMiao == ""
        ? 0
        : Number.parseFloat(strMiao) / 3600
    const digital = strDu + strFen + strMiao
    if (digital == 0) {
      return ""
    } else {
      return digital.toFixed(len)
    }
  }

  /**
   * @descripttion: 根据屏幕坐标获取场景坐标（需开启地形深度探测）
   * @param {Viewer} viewer
   * @param {Cartesian2} screenPosition
   * @return {*}
   */
  static getSceneCartesian3(
    viewer: Viewer,
    screenPosition: Cartesian2
  ): Cartesian3 | null {
    let result: Cartesian3 | null = null

    // 1.先判断浏览器是否支持深度拾取
    if (!viewer.scene.pickPositionSupported) {
      console.error("获取场景坐标失败，error:", "该浏览器不支持深度拾取！")
      return result
    }

    // 2.再判断是否开启了地形深度探测
    if (!viewer.scene.globe.depthTestAgainstTerrain) {
      console.error("获取场景坐标失败，error:", "未开启地形深度探测！")
      return result
    }

    // 3.开始获取场景坐标
    // 场景坐标，从场景的深度缓冲区中拾取相应的位置
    result = viewer.scene.pickPosition(screenPosition)

    return result
  }

  /**
   * @descripttion: 根据屏幕坐标获取视点坐标（需开启地形深度探测）
   * @param {Viewer} viewer
   * @param {Cartesian2} screenPosition
   * @return {*}
   */
  static getViewCartesian3(
    viewer: Viewer,
    screenPosition: Cartesian2
  ): Cartesian3 | null {
    let result: Cartesian3 | null = null

    // 1.先判断浏览器是否支持深度拾取
    if (!viewer.scene.pickPositionSupported) {
      console.error("获取视点坐标失败，error:", "该浏览器不支持深度拾取！")
      return result
    }

    // 2.再判断是否开启了地形深度探测
    if (!viewer.scene.globe.depthTestAgainstTerrain) {
      console.error("获取视点坐标失败，error:", "未开启地形深度探测！")
      return result
    }

    // 3.开始获取视点坐标
    // 获取视野中心点到球上的射线
    const pickRay = viewer.camera.getPickRay(screenPosition)
    // 视点坐标
    result
      = (pickRay && viewer.scene.globe.pick(pickRay, viewer.scene)) || null

    return result
  }

  /**
   * @descripttion: 根据屏幕坐标获取椭球体地表坐标（不需开启地形深度探测）
   * @param {Viewer} viewer
   * @param {Cartesian2} screenPosition
   * @return {*}
   */
  static getSurfaceCartesian3(
    viewer: Viewer,
    screenPosition: Cartesian2
  ): Cartesian3 | null {
    let result: Cartesian3 | null = null

    // 1.先判断浏览器是否支持深度拾取
    if (!viewer.scene.pickPositionSupported) {
      console.error("获取地表坐标失败，error:", "该浏览器不支持深度拾取！")
      return result
    }

    // 2.开始获取地表坐标
    result = viewer.camera.pickEllipsoid(screenPosition) || null

    return result
  }

  /**
   * @descripttion: 根据屏幕坐标获取选中对象（需开启地形深度探测）
   * @param {Viewer} viewer
   * @param {Cartesian2} screenPosition
   * @return {*}
   */
  static getPickedObject(viewer: Viewer, screenPosition: Cartesian2) {
    let result: any = null

    // 1.先判断浏览器是否支持深度拾取
    if (!viewer.scene.pickPositionSupported) {
      console.error("获取对象失败，error:", "该浏览器不支持深度拾取！")
      return result
    }

    // 2.再判断是否开启了地形深度探测
    if (!viewer.scene.globe.depthTestAgainstTerrain) {
      console.error("获取对象失败，error:", "未开启地形深度探测！")
      return result
    }

    // 3.开始获取选中对象
    result = viewer.scene.pick(screenPosition)

    return result
  }

  /**
   * 计算一组坐标组成多边形的面积
   * @param positions
   * @returns
   */
  static getPositionArea(positions: Cartesian3[]) {
    let h = 0
    for (let i = 1; i < positions.length; i++) {
      let oel = positions[i - 1]
      let el = positions[i]
      h += oel.x * el.y - el.x * oel.y
    }
    return Math.abs(h).toFixed(2)
  }
}
export default MathUtils

function transformGCS(gcs: GCS) {
  let retLat
    = -100.0
      + 2.0 * gcs.longitude
      + 3.0 * gcs.latitude
      + 0.2 * gcs.latitude * gcs.latitude
      + 0.1 * gcs.longitude * gcs.latitude
      + 0.2 * Math.sqrt(Math.abs(gcs.longitude))
  retLat
    += ((20.0 * Math.sin(6.0 * gcs.longitude * Math.PI)
      + 20.0 * Math.sin(2.0 * gcs.longitude * Math.PI))
    * 2.0)
  / 3.0
  retLat
    += ((20.0 * Math.sin(gcs.latitude * Math.PI)
      + 40.0 * Math.sin((gcs.latitude / 3.0) * Math.PI))
    * 2.0)
  / 3.0
  retLat
    += ((160.0 * Math.sin((gcs.latitude / 12.0) * Math.PI)
      + 320 * Math.sin((gcs.latitude * Math.PI) / 30.0))
    * 2.0)
  / 3.0
  let retLng
    = 300.0
      + gcs.longitude
      + 2.0 * gcs.latitude
      + 0.1 * gcs.longitude * gcs.longitude
      + 0.1 * gcs.longitude * gcs.latitude
      + 0.1 * Math.sqrt(Math.abs(gcs.longitude))
  retLng
    += ((20.0 * Math.sin(6.0 * gcs.longitude * Math.PI)
      + 20.0 * Math.sin(2.0 * gcs.longitude * Math.PI))
    * 2.0)
  / 3.0
  retLng
    += ((20.0 * Math.sin(gcs.longitude * Math.PI)
      + 40.0 * Math.sin((gcs.longitude / 3.0) * Math.PI))
    * 2.0)
  / 3.0
  retLng
    += ((150.0 * Math.sin((gcs.longitude / 12.0) * Math.PI)
      + 300.0 * Math.sin((gcs.longitude / 30.0) * Math.PI))
    * 2.0)
  / 3.0
  return new GCS(retLng, retLat)
}

/**
 * @descripttion:
 * @param {GCS} gcs
 * @return {*}
 */
function outofchina(gcs: GCS) {
  return (
    gcs.longitude < 72.004
    || gcs.longitude > 137.8347
    || gcs.latitude < 0.8293
    || gcs.latitude > 55.8271
    || false
  )
}
